package com.jewaton.authmanage.plugin;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.util.StringUtils;

import com.jewaton.authmanage.tools.Page;
import com.jewaton.authmanage.tools.jdbc.dialet.Dialect;
import com.jewaton.authmanage.tools.jdbc.dialet.MySQLDialect;

/**
 * 分页插件，不使用Mybatis的分布机制
 * 因其分页是在内存中分页，会有性能问题。
 * @author magiczeng
 */
@Intercepts( { @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class PageInterceptor implements Interceptor {
	
	private static final Log logger = LogFactory.getLog(PageInterceptor.class);
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static Dialect defaultDialect = new MySQLDialect(); // 数据库类型(默认为mysql)
    private static String defaultPageSqlId = ".*OfPage$"; // 需要拦截的ID(正则匹配)
    private static Dialect dialect = null; // 数据库类型(默认为mysql)
    private static String pageSqlId = ""; // 需要拦截的ID(正则匹配)

	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
                DEFAULT_OBJECT_WRAPPER_FACTORY);
        // 分离代理对象链(由于目标类可能被多个拦截器拦截，从而形成多次代理，通过下面的两次循环可以分离出最原始的的目标类)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
        }
        // 分离最后一个代理对象的目标类
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
        }
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        // 只重写需要分页的sql语句。通过MappedStatement的ID匹配，默认重写以Page结尾的MappedStatement的sql
        if (mappedStatement.getId().matches(pageSqlId)) {
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
            	logger.warn(mappedStatement.getId()
            			+"传入参数为空，将使用XML中的原SQL查询，如果你不想使用分页查询，请不要以[OfPage]结尾来命名sqlID。");
            	return invocation.proceed();
            } else {
            	Page page = null;
            	if(parameterObject instanceof Page){
            		// 参数本身就是Page实体
            		page = (Page) parameterObject;
            	}else if(parameterObject instanceof Map){
            		// 参数为MAP对象，该对角拥有Page属性
            		for (Entry entry : (Set<Entry>) ((Map) parameterObject).entrySet()) {
						if (entry.getValue() instanceof Page) {
							page = (Page) entry.getValue();
							break;
						}
            		}
            	}else {
            		try {
            			// 参数为某个实体，该实体拥有Page属性
                		page = (Page) metaStatementHandler
                                .getValue("delegate.boundSql.parameterObject.page");
					} catch (Exception e) {
						logger.warn(mappedStatement.getId()
		            			+"未能匹配Page对象，将使用XML中的原SQL查询，如果你不想使用分页查询，请不要以[OfPage]结尾来命名sqlID。");
						return invocation.proceed();
					}
				}
            	
            	if(page==null 
            			|| (page.getCurrentPage()<-1||page.getPageSize()<0)){
            		logger.warn(mappedStatement.getId()
	            			+"使用以[OfPage]结尾的sqlID进行查询，但不进行分页。page="+page);
            		return invocation.proceed();
            	}
            	
                String sql = boundSql.getSql();
                // 重写sql
                String pageSql = buildPageSql(sql, page);
                metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
                // 采用物理分页后，就不需要mybatis的内存分页了，所以重置下面的两个参数
                metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
                metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
                Connection connection = (Connection) invocation.getArgs()[0];
                // 重设分页参数里的总页数等
                setPageParameter(sql, connection, mappedStatement, boundSql, page);
            }
        }
        // 将执行权交给下一个拦截器
        return invocation.proceed();
	}
	
	/**
     * 根据数据库方言，生成特定的分页sql
     * 
     * @param sql
     * @param page
     * @return
     */
    private String buildPageSql(String sql, Page page) {
    	if (page != null && dialect != null) {
			//currentPage默认是从1，而已数据库是从0开始计算的．所以(page.getCurrentPage()-1)
			int pageNow = page.getCurrentPage();
			return dialect.getLimitString(sql, (pageNow<=0?0:pageNow-1)
					* page.getPageSize(), page.getPageSize());
		}
    	return sql;
    }
    
    /**
     * 从数据库里查询总的记录数并计算总页数，回写进分页参数<code>PageParameter</code>,这样调用者就可用通过 分页参数
     * <code>PageParameter</code>获得相关信息。
     * 
     * @param sql
     * @param connection
     * @param mappedStatement
     * @param boundSql
     * @param page
     */
    private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
            BoundSql boundSql, Page page) {
        // 记录总记录数
        String countSql = "select count(0) from (" + sql + ") as total";
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        try {
            countStmt = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());
            setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
            rs = countStmt.executeQuery();
            int totalCount = 0;
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }
            page.setTotalCount(totalCount);
            int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
            page.setTotalPage(totalPage);

        } catch (SQLException e) {
            logger.error("Ignore this exception", e);
        } finally {
            try {
                rs.close();
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
            try {
                countStmt.close();
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
        }

    }
    
    /**
     * 对SQL参数(?)设值
     * 
     * @param ps
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @throws SQLException
     */
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
            Object parameterObject) throws SQLException {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);
    }
    
	@Override
	public Object plugin(Object target) {
		// 当目标类是StatementHandler类型时，才包装目标类  
	    // 否则直接返回目标本身,减少目标被代理的次数  
	    if (target instanceof StatementHandler) {  
	        return Plugin.wrap(target, this);  
	    }  
	    return target;  
	}

	@Override
	public void setProperties(Properties p) {
		String dialectCls = p.getProperty("dialectClass");
		if (StringUtils.hasText(dialectCls)) {
			try {
				dialect = (Dialect) Class.forName(dialectCls)
						.getDeclaredConstructor().newInstance();
			} catch (Exception e) {
				dialect = defaultDialect;
			}
		} else {
			dialect = defaultDialect;
		}
		pageSqlId = p.getProperty("pageSqlId");//根据id来区分是否需要分页
		if (!StringUtils.hasText(pageSqlId)) {
			pageSqlId = defaultPageSqlId;
		}
	}

}
